﻿//-----------------------------------------------------------------------
// <copyright file="SmartManipulator.Size.cs" company="Nathan Miller">
// Copyright (c) Nathan Miller.
//
// Permission is hereby granted, free of charge, to any person obtaining
// a copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to
// permit persons to whom the Software is furnished to do so, subject to
// the following conditions:
// 
// The above copyright notice and this permission notice shall be
// included in all copies or substantial portions of the Software.
// 
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
// </copyright>
//-----------------------------------------------------------------------
namespace TileTool.Base.Manipulator.Smart
{
    using System;
    using System.Diagnostics;
    using TileTool.Common.Math;
    using TileTool.Editor.Tool.Scene;
    using TileTool.Scene.Entity;

    /// <summary>
    /// Size implementation of the smart manipulator.
    /// </summary>
    public partial class SmartManipulator
    {
        #region Methods - Public

        /// <summary>
        /// Gets the size manipulation amount for the manipulator.
        /// 
        /// Trans will contain the amount to translate the object and size will
        /// contain the amount to add to the size of each object.
        /// 
        /// If the entity parameter is null, will return the size amount given the manipulator's
        /// current matrix. If the entity parameter is non-null, will return the size amount
        /// given that entity's matrix.
        /// </summary>
        /// <param name="entity">Entity to get manipulation amount for if non-null.</param>
        /// <param name="trans">Translate amount.</param>
        /// <param name="size">Size amount.</param>
        public void Size_GetAmount(IEntity entity, out Vector2D trans, out Vector2D size)
        {
            if (this.ManipulateType != SmartManipulateType.Size)
            {
                throw new InvalidOperationException("Must be sizing to use this function.");
            }

            trans = Vector2D.Zero;
            size = Vector2D.Zero;

            Vector2D dragAmount = DragAmountWorld;
            SmartManipulatorHandlePosition handlePos = (SmartManipulatorHandlePosition)DragHandle.Id;

            if (entity != null)
            {
                Matrix2D matNoPos = entity.Matrix.RotationOnly();

                matNoPos = matNoPos.Invert();

                // Get the drag amount in the entity's local space since
                // our handle matrix is currently identity for multi
                // select.
                dragAmount = matNoPos * dragAmount;
                handlePos = this.GetNearestHandlePos(entity, handlePos);
            }

            this.GetManipulateAmountForHandle(handlePos, dragAmount, ref trans, ref size);

            // We need to put the translate amount into the entity's space
            // so that things look correct when the entity is rotated.
            Matrix2D transformMat;
            if (entity != null)
            {
                transformMat = entity.Matrix.RotationOnly();
            }
            else
            {
                transformMat = this.Matrix.RotationOnly();
            }

            trans = transformMat * trans;
        }

        #endregion

        #region Methods - Private

        /// <summary>
        /// Setup size handles.
        /// </summary>
        private void Size_SetupHandles()
        {
            this.AllocateAndAddSizeHandle(SmartManipulatorHandlePosition.North);
            this.AllocateAndAddSizeHandle(SmartManipulatorHandlePosition.South);
            this.AllocateAndAddSizeHandle(SmartManipulatorHandlePosition.East);
            this.AllocateAndAddSizeHandle(SmartManipulatorHandlePosition.West);
            this.AllocateAndAddSizeHandle(SmartManipulatorHandlePosition.NorthWest);
            this.AllocateAndAddSizeHandle(SmartManipulatorHandlePosition.NorthEast);
            this.AllocateAndAddSizeHandle(SmartManipulatorHandlePosition.SouthWest);
            this.AllocateAndAddSizeHandle(SmartManipulatorHandlePosition.SouthEast);
        }

        /// <summary>
        /// Called when the manipulator is being reset.
        /// </summary>
        private void Size_Reset()
        {
        }

        /// <summary>
        /// Called when a drag begins.
        /// </summary>
        /// <param name="sceneInputData">Scene input data.</param>
        /// <param name="canvasPos">Canvas position position where the drag started.</param>
        private void Size_DragBegin(SceneToolInputData sceneInputData, Vector2D canvasPos)
        {
        }

        /// <summary>
        /// Called when a drag ends.
        /// </summary>
        private void Size_DragEnd()
        {
        }

        /// <summary>
        /// Called when a drag update happens.
        /// </summary>
        /// <param name="sceneInputData">Scene input data.</param>
        /// <param name="canvasPos">Canvas position position where the mouse is currently.</param>
        private void Size_DragUpdate(SceneToolInputData sceneInputData, Vector2D canvasPos)
        {
        }

        /// <summary>
        /// Called to update the size handle positions.
        /// </summary>
        private void Size_UpdateHandlePositions()
        {
            Rect2D boundingRect = new Rect2D(Vector2D.Zero, this.Size);
            Vector2D[] rectPointsLocal = boundingRect.GetPoints();

            this.PositionHandle(SmartManipulatorHandlePosition.SouthWest, rectPointsLocal[0]);
            this.PositionHandle(SmartManipulatorHandlePosition.SouthEast, rectPointsLocal[1]);
            this.PositionHandle(SmartManipulatorHandlePosition.NorthEast, rectPointsLocal[2]);
            this.PositionHandle(SmartManipulatorHandlePosition.NorthWest, rectPointsLocal[3]);
            this.PositionHandle(SmartManipulatorHandlePosition.South, (rectPointsLocal[0] + rectPointsLocal[1]) * 0.5f);
            this.PositionHandle(SmartManipulatorHandlePosition.North, (rectPointsLocal[2] + rectPointsLocal[3]) * 0.5f);
            this.PositionHandle(SmartManipulatorHandlePosition.East, (rectPointsLocal[1] + rectPointsLocal[2]) * 0.5f);
            this.PositionHandle(SmartManipulatorHandlePosition.West, (rectPointsLocal[0] + rectPointsLocal[3]) * 0.5f);
        }

        /// <summary>
        /// Allocate and add a handle with the passed position.
        /// </summary>
        /// <param name="pos">Position of the handle.</param>
        private void AllocateAndAddSizeHandle(SmartManipulatorHandlePosition pos)
        {
            SmartManipulatorHandle handle = new SmartManipulatorHandle();

            handle.Id = (int)pos;
            handle.Size = new Size2D(this.settings.SizeCircleRadius);
            handle.Pos = Vector2D.Zero;
            handle.DragStartPos = Vector2D.Zero;
            handle.IdleColor = this.settings.SizeCircleColor;
            handle.SelectedColor = this.settings.SizeCircleSelectedColor;

            this.AddManipulatorHandle(handle);
        }

        /// <summary>
        /// Given an entity and a reference handle position, find the handle that is closest to the
        /// reference handle position on the entity when positioned in the world.
        /// </summary>
        /// <param name="entity">Entity whose transform we will use.</param>
        /// <param name="referenceHandle">Reference handle position.</param>
        /// <returns>Returns the handle position nearest to referenceHandle when transformed using
        /// the entity's matrix.</returns>
        private SmartManipulatorHandlePosition GetNearestHandlePos(IEntity entity, SmartManipulatorHandlePosition referenceHandle)
        {
            int nearest = -1;
            float nearDistanceSq = float.MaxValue;
            Vector2D refHandlePos = this.GetHandleBasePosition(referenceHandle);

            foreach (SmartManipulatorHandlePosition handlePos in Enum.GetValues(typeof(SmartManipulatorHandlePosition)))
            {
                switch (handlePos)
                {
                    case SmartManipulatorHandlePosition.Rotate:
                    case SmartManipulatorHandlePosition.RotateCenter:
                        continue;
                }

                Matrix2D matNoTrans = entity.Matrix.RotationOnly();
                Vector2D pos = matNoTrans * this.GetHandleBasePosition(handlePos);
                float distSq = refHandlePos.DistanceSq(pos);
                if (distSq < nearDistanceSq)
                {
                    nearest = (int)handlePos;
                    nearDistanceSq = distSq;
                }
            }

            Debug.Assert(nearest != -1, "We should always find a nearest handle.");

            return (SmartManipulatorHandlePosition)nearest;
        }

        /// <summary>
        /// Get the manipulation amount in handle local space for a drag amount.
        /// </summary>
        /// <param name="handlePos">Handle we are manipulating.</param>
        /// <param name="dragAmount">Draw amount in world space.</param>
        /// <param name="trans">Output translate amount in handle local space.</param>
        /// <param name="size">Output size amount in handle local space.</param>
        private void GetManipulateAmountForHandle(SmartManipulatorHandlePosition handlePos, Vector2D dragAmount, ref Vector2D trans, ref Vector2D size)
        {
            ManipulatorHandle handle = this.GetHandle(handlePos);
            Matrix2D handleNoPosMatrix = this.Matrix.RotationOnly();
            Matrix2D invMatrix = handleNoPosMatrix.Invert();
            Vector2D localAmount = invMatrix.Multiply(dragAmount);
            Vector2D dragX = Vector2D.AxisX * Vector2D.AxisX.Dot(localAmount);
            Vector2D dragY = Vector2D.AxisY * Vector2D.AxisY.Dot(localAmount);

            switch (handlePos)
            {
                case SmartManipulatorHandlePosition.North:
                    size = dragY;
                    break;
                case SmartManipulatorHandlePosition.South:
                    size = -dragY;
                    trans = dragY;
                    break;
                case SmartManipulatorHandlePosition.East:
                    size = dragX;
                    break;
                case SmartManipulatorHandlePosition.West:
                    size = -dragX;
                    trans = dragX;
                    break;
                case SmartManipulatorHandlePosition.NorthWest:
                    size = -dragX + dragY;
                    trans = dragX;
                    break;
                case SmartManipulatorHandlePosition.NorthEast:
                    size = dragX + dragY;
                    break;
                case SmartManipulatorHandlePosition.SouthWest:
                    size = -dragX - dragY;
                    trans = dragX + dragY;
                    break;
                case SmartManipulatorHandlePosition.SouthEast:
                    size = dragX - dragY;
                    trans = dragY;
                    break;
            }
        }

        /// <summary>
        /// Given a handle position, get its base position point.
        /// </summary>
        /// <param name="pos">Handle position to get.</param>
        /// <returns>Returns the handle's base position.</returns>
        private Vector2D GetHandleBasePosition(SmartManipulatorHandlePosition pos)
        {
            switch (pos)
            {
                case SmartManipulatorHandlePosition.North:
                    return new Vector2D(0.0f, 1.0f);
                case SmartManipulatorHandlePosition.South:
                    return new Vector2D(0.0f, -1.0f);
                case SmartManipulatorHandlePosition.East:
                    return new Vector2D(1.0f, 0.0f);
                case SmartManipulatorHandlePosition.West:
                    return new Vector2D(-1.0f, 0.0f);
                case SmartManipulatorHandlePosition.NorthWest:
                    return new Vector2D(-1.0f, 1.0f);
                case SmartManipulatorHandlePosition.NorthEast:
                    return new Vector2D(1.0f, 1.0f);
                case SmartManipulatorHandlePosition.SouthWest:
                    return new Vector2D(-1.0f, -1.0f);
                case SmartManipulatorHandlePosition.SouthEast:
                    return new Vector2D(1.0f, -1.0f);
            }

            return Vector2D.Zero;
        }

        #endregion
    }
}